# -*- coding: utf-8 -*-
""" The LAS Object Header module.

  :Author:
    - 2010-2012 Nicola Creati and Roberto Vidmar

  :Revision:  $Revision: 5 $
              $Date: 2012-11-08 14:52:33 +0100 (Thu, 08 Nov 2012) $

  :Copyright: 2010-2012
              Nicola Creati <ncreati@inogs.it>
              Roberto Vidmar <rvidmar@inogs.it>

  :License: MIT/X11 License (see :download:`LICENSE.txt
                             <../../LICENSE.txt>`)
"""

import struct
import time
import types
import uuid
import cPickle
import numpy as np
try:
  from osgeo import osr
except ImportError:
  osr = None
import string

from geotiffKeys import GeotiffKeys
from geotif import normalize, geoKeysFromWkt
from lasExceptions import (InvalidPointDataFormatID, MustBeImplemented,
  UnimplementedException, InvalidLASFile)

# Lower first string letter
fl = lambda s: s[:1].lower() + s[1:] if s else ''


def convert(record):
    # Translate record
    unprt=''
    myprintable = (string.letters + string.digits + string.punctuation + '\t \n')

    geoAscii = ""
    for c in record:
        if myprintable.find(c) >= 0:
            geoAscii += c
        else:
            geoAscii += unprt
    return geoAscii

#-------------------------------------------------------------------------------
def sizeBytes(dataType, n):
  """ Return size in bytes of n instances of data of type `dataType`

    :param dataType: any in (cbBhHiIqQfd)
    :type  dataType: char
    :param n: number of instances
    :type  n: int
    :returns: size in bytes
    :rtype: int
    :raises: :class:`TypeError`
  """
  if dataType in (AbstractRecord.INT8, AbstractRecord.UINT8,
    AbstractRecord.SINT8, AbstractRecord.SCHAR):
    b = 1
  elif dataType in (AbstractRecord.INT16, AbstractRecord.UINT16):
    b = 2
  elif dataType in (AbstractRecord.INT32, AbstractRecord.UINT32,
    AbstractRecord.FLOAT):
    b = 4
  elif dataType in (AbstractRecord.INT64, AbstractRecord.UINT64,
    AbstractRecord.DOUBLE):
    b = 8
  else:
    raise TypeError("Data type '%s' not understood" % dataType)
  return b * n

#-------------------------------------------------------------------------------
def getLongest(l):
  """ Return length of longest string in list l *OR*
             length of longest line in string l

    :param l: list of strings or string of lines
    :type  l: tuple, list or string
    :returns: length of longest string in list l *OR*
              length of longest line in string l
    :rtype: int
  """
  if isinstance(l, (list, tuple)):
    return max([len(i.splitlines()[0]) for i in l])
  else:
    return max([len(i) for i in l.split('\n')])

#-------------------------------------------------------------------------------
def addGet(self, name):
  """ Add getter method for attribute `name`
  """
  def get(self):
    return eval('self._%s' % name)
  return get

#-------------------------------------------------------------------------------
def addSet(self, name, value):
  """ Add setter method for attribute `name`
  """
  def set(self, value):
    setattr(self, '_%s' % name, value)
  return set


#-------------------------------------------------------------------------------
def addGetN(self, name):
  """ Add getter method for attribute `name`
  """
  def get(self):
    return eval("self._record['%s']" % name)
  return get


#-------------------------------------------------------------------------------
def nonull(s):
  """ Return s truncated to the first null character

    :param s: string to truncate
    :type  s: string
    :returns: s truncated to the first null character
    :rtype: string
  """
  return s.split('\x00')[0]

#-------------------------------------------------------------------------------
def string_clean(s):
  """ Remove unprintable characters from string s and cut it at first
      null character.

    :param s: string to clean
    :type  s: string
    :returns: s with all unprintable characters removed and truncated to first
              null character
    :rtype: string
  """
  try:
    s + ''
  except:
    return s
  else:
    # Define my own "printable" charset
    ascii256 = "".join(map(chr, range(256)))
    printable = "".join(map(chr, range(32, 127)))
    flt = "".join(('.', c) [c in printable] for c in ascii256)
    return nonull(s).translate(flt)

#-------------------------------------------------------------------------------
def pad_with_nulls(instring, l):
  """ Pad string instring to length l with null (ASCII 0) characters
      Truncate instring to l characters if necessary

    :param instring: string to pad
    :type  instring: string
    :returns: instring padded with null characters to length l
    :rtype: string
  """
  return instring + '\x00' * max(0, (l - len(instring)))

#===============================================================================
class AbstractRecord(object):
  """ An abstract class for reading and writing structured records.

  .. warning:: **MUST** be subclassed
  """
  # Size 1
  INT8    = CHAR    = 'c'
  SINT8   = SCHAR   = 'b'
  UINT8   = UCHAR   = 'B' # B is actually unsigned byte
  # Size 2
  INT16   = SHORT   = 'h'
  UINT16  = USHORT  = 'H'
  # Size 4
  INT32   = LONG    = 'i'
  UINT32  = ULONG   = 'I'
  FLOAT   = 'f'
  # Size 8
  INT64   = LLONG   = 'q'
  UINT64  = ULLONG  = 'Q'
  DOUBLE  = 'd'

  def _createStructure(self, structure):
    """ Create structure for the current object, define
        methods to get and set attributes at run time
        An example of structure:

        s = (
          ('Red',              AbstractRecord.USHORT,  1),
          ('Green',            AbstractRecord.USHORT,  1),
          ('Blue',             AbstractRecord.USHORT,  1),)

      :param structure: Tuple defining record structure
      :type  structure: tuple
    """
    self.structure = structure
    for field in structure:
      self.__dict__['_%s' % field[0]] = None

    # Automatically add methods
    for key, dtype, n in structure:
      # Add get method for each header item
      setattr(self, '%s' % fl(key),
        types.MethodType(addGet(self, key), self, self.__class__))
      exec("self.%s.__func__.__doc__ = 'Return `_%s` attribute'" %
       (fl(key), fl(key)))

      # Add set method for each header item
      value = eval('self._%s' % key)
      setattr(self, 'set%s' % key,
        types.MethodType(addSet(self, key, value), self, self.__class__))
      # Add function documentation through black magic
      exec("self.set%s.__func__.__doc__ = 'Set `_%s` attribute to `value`'" %
       (key, key))

  def _read(self, source):
    """ Load structure from file or string

      :param source: where to read data from
      :type  source: file or string
      :returns: number of bytes read
      :rtype: int
      :raises: :class:`IOError`
    """
    if not self.isReadMode():
      raise IOError("%s not opened for reading" %
        self.__class__.__name__)

    # Read record header
    bytesRead = 0
    for key, dtype, n in self.structure:
      nbytes = struct.calcsize(dtype) * n
      if isinstance(source, file):
        string = source.read(nbytes)
      else:
        string = source[bytesRead: bytesRead + nbytes]

      if dtype in (self.CHAR, self.UCHAR):
        # string
        value = string
        if n == 1:
          # Format is char but content is a NUMBER!
          value = struct.unpack(dtype, string)[0]
          #value = ord(value)
      else:
        value = struct.unpack(dtype * n, string)
        if n == 1:
          # single binary number
          value = value[0]
      bytesRead += nbytes

      # Set attribute according to Record format
      self.__setattr__('_%s' % key, value)
    return bytesRead

  def sizeBytes(self):
    """ Return size in bytes of this record

      :returns: size in bytes of this record
      :rtype: int
    """
    return sum([sizeBytes(*item[1:]) for item in self.structure])

  def fieldSize(self, field):
    """ Return field size in bytes

      :param field: field name
      :type  field: string
      :returns: size in bytes of `field` in this record
      :rtype: int
    """
    for key, dtype, n in self.structure:
      if key == field:
        return sizeBytes(dtype, n)

  def allFields(self):
    """ Return list of all fields in this record

      :returns: list of all fields in this record
      :rtype: list
    """
    return [key for key, dtype, n in self.structure]

  def isReadMode(self):
    """ Return True if object is created for reading

      :raises: :class:`~ALASpy.lasExceptions.MustBeImplemented`

      .. warning:: **MUST BE REIMPLEMENTED**
    """
    raise MustBeImplemented("isReadMode method must be implemented in subclass")

#===============================================================================
class PointDataRecord(AbstractRecord):
  """ LAS Point Data Record

    .. note:: Formats from 0 to 10 are implemented.
  """
  # (Description,        type,    array size)
  Core14 = (
    ('X',                AbstractRecord.LONG,    1),
    ('Y',                AbstractRecord.LONG,    1),
    ('Z',                AbstractRecord.LONG,    1),
    ('Intensity',        AbstractRecord.USHORT,  1),)
  Core5 = (
    ('Classification',   AbstractRecord.UCHAR,   1),
    ('ScanAngleRank',    AbstractRecord.SCHAR,   1),
    ('UserData',         AbstractRecord.UCHAR,   1),
    ('PointSourceid',    AbstractRecord.USHORT,  1),)
  Core5New = (
    ('Classification',   AbstractRecord.UCHAR,   1),
    ('UserData',         AbstractRecord.UCHAR,   1),
    ('ScanAngleRank',    AbstractRecord.SCHAR,   1),
    ('PointSourceid',    AbstractRecord.USHORT,  1),)
  GPSTime = (
    ('GPSTime',          AbstractRecord.DOUBLE,  1),)
  NIR = (
    ('NIR',              AbstractRecord.DOUBLE,  1),)
  RGB = (
    ('Red',              AbstractRecord.USHORT,  1),
    ('Green',            AbstractRecord.USHORT,  1),
    ('Blue',             AbstractRecord.USHORT,  1),)
  WavePackets = (
    ('WavePacketDescrIndex',   AbstractRecord.UCHAR,  1),
    ('ByteOffsetToWaveData',   AbstractRecord.ULLONG, 1),
    ('WaveformPacketSize',     AbstractRecord.ULONG,  1),
    ('ReturnPointWaveformLoc', AbstractRecord.FLOAT,  1),
    ('X_at_t',                 AbstractRecord.FLOAT,  1),
    ('Y_at_t',                 AbstractRecord.FLOAT,  1),
    ('Z_at_t',                 AbstractRecord.FLOAT,  1),)

  Format0 = (Core14 + (
    ('AByte',            AbstractRecord.UCHAR,   1),) +
    Core5)
  Format1 = (Format0 + GPSTime)
  Format2 = (Format0 + RGB)
  Format3 = (Format1 + RGB)
  Format4 = (Format1 + WavePackets)
  Format5 = (Format1 + RGB + WavePackets)

  Format6 = (Core14 + (
    ('TwoBytes',           AbstractRecord.UINT8,  2),) +
    Core5New + GPSTime)
  Format7 = (Format6 + RGB)
  Format8 = (Format7 + NIR)
  Format9 = (Format6 + WavePackets)
  Format10 = (Format7 + WavePackets)

  OneByteFields = (
    'Return Number',
    'Number Of Returns',
    'Scan Direction Flag',
    'Edge Of Flight Line',
    )
  TwoByteFields = OneByteFields + (
    'Classification Flags',
    'Scanner Channel',
    )
  def __init__(self, header, record=None, index=None):
    """ Create a new Point Data Record instance.

      .. note:: If index is not specified no record is actually read.
      ..

      :param header: Las Header
      :type  header: :class:`Header` instance
      :raises: :class:`~ALASpy.lasExceptions.UnimplementedException`
    """
    self._header = header
    self._record = record
    self._index = index
    self._format = header.pointDataFormatID()
    if self._format > 3:
      raise UnimplementedException(
        "PointDataFormat %d not yet tested" % self._format)
    structure = self.__getattribute__('Format%s' % self._format)
    self._createStructure(structure)

  def _createStructure(self, structure):
    """ Create structure for the current object, define
        methods to get and set attributes at run time
        An example of structure:

        s = (
          ('Red',              AbstractRecord.USHORT,  1),
          ('Green',            AbstractRecord.USHORT,  1),
          ('Blue',             AbstractRecord.USHORT,  1),)

      :param structure: Tuple defining record structure
      :type  structure: tuple
    """
    self.structure = structure
    for field in structure:
      self.__dict__['_%s' % field[0]] = None
    # Automatically add methods
    for key, dtype, n in structure:
      # Add get method for each header item
      setattr(self, '%s' % fl(key),
        types.MethodType(addGetN(self, key), self, self.__class__))
      exec("self.%s.__func__.__doc__ = 'Return `_%s` attribute'" %
       (fl(key), fl(key)))

    # For now point data is only read-pnly
#      # Add set method for each header item
#      value = eval('self._%s' % key)
#      setattr(self, 'set%s' % key,
#        types.MethodType(addSetN(self, key, value), self, self.__class__))
#      # Add function documentation through black magic
#      exec("self.set%s.__func__.__doc__ = 'Set `_%s` attribute to `value`'" %
#       (key, key))


  def fieldInfo(self, field, xyzType):
    """ Return field dtype and number of elements.

      :param field: field name
      :type  field: string
      :param xyzType: dtype for X, Y and Z
      :type  xyzType: numpy.dtype
    """
    for key, dtype, n in self.structure:
      if key == field:
        if key in 'XYZ':
          return xyzType
        else:
          return dtype, n

  def makeDtype(self, fields='all', skip=None, xyzType=np.int32, dummies=True):
    """ Return dtype for reading / writing

      :param fields:  list of field names to read (default is all fields)
      :type  fields:  list of strings
      :param skip:    list of field names to skip (default is skip None)
      :type  skip:    list of strings
      :param xyzType: X, Y, Z will have this data type (default is np.int32)
      :type  xyzType: numpy.dtype
      :param dummies: if True add dummy fields for reading
      :type  dummies: bool
    """
    allfields = self.allFields()

    if fields == 'all':
      fields = allfields

    formats = []
    names = []
    i = 0
    for f in allfields:
      if fields.__contains__(f):
        formats.append(self.fieldInfo(f, xyzType))
        names.append(f)
      elif dummies:
        formats.append('V' + str(self.fieldSize(f)))
        names.append('dummy' + str(i))
        i += 1

    if skip and dummies:
      names.append('dummy' + str(i))
      formats.append('V' + str(self.sizeBytes() * skip))

    return np.dtype({'names': names, 'formats': formats})

  def __repr__(self):
    """ :class:`PointDataRecord` representation with format and index (if any)
    """
    cname = self.__class__.__name__
    if self._record is None:
      return '%s[%s]:\n' % (cname, None)

    return '%s[%d], Format %d:\n%s' % (
      cname, self._index, self._format, self.__str__())

  def __str__(self):
    """ :class:`PointDataRecord` representation
    """
    if self._record is None:
      return None

    if self._format < 6:
      bitFieldNames = self.OneByteFields
    else:
      bitFieldNames = self.TwoByteFields
    ml = getLongest(
      tuple([key for key, dtype, n in self.structure]) + bitFieldNames)
    ff = ' %%-%ds : %%s\n' % ml
    s = ''
    for key, dtype, n in self.structure:
      if key == 'X':
        s += ff % (key, self.xs())
      elif key == 'Y':
        s += ff % (key, self.ys())
      elif key == 'Z':
        s += ff % (key, self.zs())
      elif key == 'AByte':
        s += ff % ('Return Number', self.returnNumber())
        s += ff % ('Number Of Returns', self.numberOfReturns())
        s += ff % ('Scan Direction Flag', self.scanDirectionFlag())
        s += ff % ('Edge Of Flight Line', self.edgeOfFlightLine())
      elif key == 'TwoBytes':
        s += ff % ('Return Number', self.returnNumber())
        s += ff % ('Number Of Returns', self.numberOfReturns())
        s += ff % ('Classification Flags', self.classificationFlags())
        s += ff % ('Scanner Channel', self.scannerChannel())
        s += ff % ('Scan Direction Flag', self.scanDirectionFlag())
        s += ff % ('Edge Of Flight Line', self.edgeOfFlightLine())
      elif key == 'Classification':
        s += ff % (key, self.classification())
      elif key == 'ScanAngleRank':
        s += ff % (key, self.scanAngleRank())
      elif key == 'UserData':
        s += ff % (key, self.userData())
      elif key == 'GPSTime':
        s += ff % (key, self.time())
      else:
        s += ff % (key,
          string_clean('%s' % self._record[key]))
    return s

  @classmethod
  def getReturnNumber(cls, data):
    """ Return 'Return Number' from `data` according to size in bytes

      :param data: one or two bytes to be interpreted
      :type  data: numpy array
    """
    if data.itemsize == 1:
      return (data & 0b11100000) >> 5
    else:
      return (data[0] & 0b11110000) >> 4

  def returnNumber(self):
    """ Return `Return Number` for this point:

        bits 0, 1, 2 (if Point format < 6) or
        bits 0, 1, 2, 4

      :returns: 'Return Number' for this point
      :rtype: int
    """
    if self._format < 6:
      return (self._record['AByte'] & 0b11100000) >> 5
    else:
      return (self._record['TwoBytes'][0] & 0b11110000) >> 4

  def numberOfReturns(self):
    """ Return 'Number of Returns' for this point:

        bits 3, 4, 5 (if Point format < 6) or
        bits 4, 5, 6, 7

      :returns: 'Number of Returns' for this point
      :rtype: int
    """
    if self._format < 6:
      return (self._record['AByte'] & 0b00011100) >> 2
    else:
      return (self._record['TwoBytes'][0] & 0b00001111)

  def classificationFlags(self):
    """ Return 'Classification Flags' for this point:

        bits 0 - 3

      :returns: 'Classification Flags' for this point
      :rtype: int
    """
    return (self._record['TwoBytes'][1] & 0b11110000) >> 4

  def scannerChannel(self):
    """ Return 'Scanner Channel' for this point:

        bits 4, 5

      :returns: 'Scanner Channel' for this point
      :rtype: int
    """
    return (self._record['TwoBytes'][1] & 0b00001100) >> 2

  def scanDirectionFlag(self):
    """ Return 'Scan Direction Flag' for this point:

        bit 6

      :returns: 'Scan Direction Flag' for this point
      :rtype: bool
    """
    if self._format < 6:
      return bool((self._record['AByte'] & 0b00000010) >> 1)
    else:
      return bool((self._record['TwoBytes'][1] & 0b00000010) >> 1)

  def edgeOfFlightLine(self):
    """ Return 'Edge Of Flight Line' for this point:

        bit 7

      :returns: 'Edge Of Flight Line'
      :rtype: bool
    """
    if self._format < 6:
      return bool(self._record['AByte'] & 0b00000001)
    else:
      return bool(self._TwoBytes[1] & 0b00000001)

  def classification(self):
    """ Return classification integer value

      :returns: Classification integer value
      :rtype: int
    """
    return self._record['Classification'] & 0b11111

  def userData(self):
    """ Return User Data integer value

      :returns: User Data integer value
      :rtype: int
    """
    return self._UserData

  def scanAngleRank(self):
    """ Return Scan Angle Rank integer value

      :returns: Scan Angle Rank integer value
      :rtype: int
    """
    return self._record['ScanAngleRank']

  def record(self):
    """ Return point index

      :returns: point index in LAS file
      :rtype: int
    """
    return self._record

  def time(self):
    """ Return record time stamp (GPS Time)

      :returns: record time stamp
      :rtype: double
    """
    shift = 0
    if self._header.versionMinor() == 2:
      if self._header.globalEncoding() == 1:
        shift = 1e9
    return self._record['GPSTime'] - shift

  def xs(self):
    """ Return real x value (corrected for offset and scale)

      :returns: x value
      :rtype: float
    """
    return self._record['X'] * self._header.xscaleFactor() + self._header.xoffset()

  def ys(self):
    """ Return real y value (corrected for offset and scale)

      :returns: y value
      :rtype: float
    """
    return self._record['Y']* self._header.yscaleFactor() + self._header.yoffset()

  def zs(self):
    """ Return real z value (corrected for offset and scale)

      :returns: z value
      :rtype: float
    """
    return self._record['Z'] * self._header.zscaleFactor() + self._header.zoffset()

  def isReadMode(self):
    """ Return True if Header is in 'read' mode

      :returns: True if Header is in 'read' mode
      :rtype: bool
    """
    return self._header.isReadMode()

#===============================================================================
class Header(AbstractRecord):
  """ The LAS file header class

  .. note:: Formats from 1.0 to 1.4 are implemented.
  """
  GeneratingSoftware = 'ALASpy'
  # LAS 1.0
  Las10 = (
    ('FileSignature',      AbstractRecord.CHAR,   4),
    ('Reserved'     ,      AbstractRecord.ULONG,  1),
    ('ProjectID_GUID1',    AbstractRecord.ULONG,  1),
    ('ProjectID_GUID2',    AbstractRecord.USHORT, 1),
    ('ProjectID_GUID3',    AbstractRecord.USHORT, 1),
    ('ProjectID_GUID4',    AbstractRecord.UCHAR,  8),
    ('VersionMajor',       AbstractRecord.UCHAR,  1),
    ('VersionMinor',       AbstractRecord.UCHAR,  1),
    ('SystemIdentifier',   AbstractRecord.CHAR,  32),
    ('GeneratingSoftware', AbstractRecord.CHAR,  32),
    ('FileCreationDoY',    AbstractRecord.USHORT, 1),
    ('FileCreationYear',   AbstractRecord.USHORT, 1),
    ('HeaderSize',         AbstractRecord.USHORT, 1),
    ('OffsetToPointData',  AbstractRecord.ULONG,  1),
    ('NumVarLenRecords',   AbstractRecord.ULONG,  1),
    ('PointDataFormatID',  AbstractRecord.UCHAR,  1),
    ('PointDataReclen',    AbstractRecord.USHORT, 1),
    ('NumPointRecords',    AbstractRecord.ULONG,  1),
    ('NumPointsByReturn',  AbstractRecord.ULONG,  5),
    ('XscaleFactor',       AbstractRecord.DOUBLE, 1),
    ('YscaleFactor',       AbstractRecord.DOUBLE, 1),
    ('ZscaleFactor',       AbstractRecord.DOUBLE, 1),
    ('Xoffset',            AbstractRecord.DOUBLE, 1),
    ('Yoffset',            AbstractRecord.DOUBLE, 1),
    ('Zoffset',            AbstractRecord.DOUBLE, 1),
    ('Xmax',               AbstractRecord.DOUBLE, 1),
    ('Xmin',               AbstractRecord.DOUBLE, 1),
    ('Ymax',               AbstractRecord.DOUBLE, 1),
    ('Ymin',               AbstractRecord.DOUBLE, 1),
    ('Zmax',               AbstractRecord.DOUBLE, 1),
    ('Zmin',               AbstractRecord.DOUBLE, 1),
  )
  # LAS 1.1
  Las11 = (Las10[0], ) + (
    ('FileSourceID',       AbstractRecord.USHORT, 1),
    ('Reserved',           AbstractRecord.USHORT, 1),
  ) + Las10[2:]
  # LAS 1.2
  Las12 = Las11[0:2] + (
    ('GlobalEncoding',     AbstractRecord.USHORT, 1),
  ) + Las11[3:]
  # LAS 1.3
  Las13 = Las12[:] + (
    ('StartOfWaveformDataPacketRec',  AbstractRecord.ULLONG,   1),
  )
  # LAS 1.4
  Las14 = Las13[:] + (
    ('StartOfFirstExtendedVLR', AbstractRecord.ULLONG,  1),
    ('NumberOfExtendedVLR',     AbstractRecord.ULONG,   1),
    ('NumberOfPointRecords',    AbstractRecord.ULLONG,  1),
    ('NumberOfPointsByReturn',  AbstractRecord.ULLONG, 15),
  )

  validPointDataFormats = {
    (1, 0): range(2),
    (1, 1): range(2),
    (1, 2): range(4),
    (1, 3): range(6),
    (1, 4): range(10),
  }

  def __init__(self, parent, version=(1, 0), pointDataFormatID=0):
    """ Create a new :class:`Header` instance

      :param parent: LasObject or opened file
      :type  parent: :class:`LasObject` or opened file
      :param version: LAS file version
                      (ignored in read mode)
      :type  version: tuple
      :param pointDataFormatID: LAS point data format (ignored in read mode)
      :type pointDataFormatID: int
    """
    self._warnings = ""

    # Las file object
    self._parent = parent
    # No geoinfo yet
    self._wkt = None
    self._proj4 = None

    # Read if in read mode or create appropriate header structure
    if self.isReadMode():
      # Retrieve LAS file version
      self.fid().seek(24)
      v = (self.fid().read(1), self.fid().read(1))
      self.fid().seek(0)
      try:
        self._version = (ord(v[0]), ord(v[1]))
      except TypeError:
        raise InvalidLASFile(
          "LAS file '%s' is invalid: no version information." %
          self._parent.filename())
      structure = self.__getattribute__('Las%s%s' % self._version)
      self._createStructure(structure)
      self._read()

      # Create GUID from ProjectID_GUID fields
      guid = (struct.pack(AbstractRecord.ULONG, self._ProjectID_GUID1) +
        struct.pack(AbstractRecord.USHORT, self._ProjectID_GUID2) +
        struct.pack(AbstractRecord.USHORT, self._ProjectID_GUID3) +
        self._ProjectID_GUID4)
      self._guid = uuid.UUID(bytes_le=guid)
      # Read all records
      self._varLenRecords = []
      for dummy in range(self.numVarLenRecords()):
        vlr = VLRecord(self)
        # The key is (User_ID, Record_ID )
        self._varLenRecords.append(vlr)
    else:
      # CREATION MODE
      if not isinstance(version, tuple):
        self._version = (1, version)
        self._warnings += "Version is NOT a tuple: set to %s.%s" % self._version
      else:
        self._version = version
      structure = self.__getattribute__('Las%s%s' % self._version)
      self._createStructure(structure)

      # Create a NEW GUID
      self._guid = uuid.uuid4()

      # Define default header structure and attributes
      self._FileSignature      = 'LASF'
      self._Reserved           = 0
      if self._version >= (1, 1):
        self._FileSourceID     = 0
      if self._version >= (1, 2):
        self._GlobalEncoding   = 0
      self._ProjectID_GUID1 = struct.unpack(
        AbstractRecord.ULONG, self._guid.bytes[:4])[0]
      self._ProjectID_GUID2 = struct.unpack(
        AbstractRecord.USHORT, self._guid.bytes[4:6])[0]
      self._ProjectID_GUID3 = struct.unpack(
        AbstractRecord.USHORT, self._guid.bytes[6:8])[0]
      self._ProjectID_GUID4 = self._guid.bytes[8:]
      self._VersionMajor       = self._version[0]
      self._VersionMinor       = self._version[1]
      self._SystemIdentifier   = 'None'.ljust(
        self._getN4Key('SystemIdentifier'))
      self._GeneratingSoftware = pad_with_nulls(self.GeneratingSoftware,
        self._getN4Key('GeneratingSoftware'))
      self._FileCreationDoY    = int(time.gmtime().tm_yday)
      self._FileCreationYear   = int(time.asctime().split()[-1])
      self._HeaderSize         = self.sizeBytes()
      self._OffsetToPointData   = self._HeaderSize
      self._NumVarLenRecords   = 0
      self._setPDFID(pointDataFormatID)
      self._NumPointRecords     = 0
      self._NumPointsByReturn  = (0, 0, 0, 0, 0)
      self._XscaleFactor       = 0.01
      self._YscaleFactor       = 0.01
      self._ZscaleFactor       = 0.01
      self._Xoffset            = 0
      self._Yoffset            = 0
      self._Zoffset            = 0
      self._Xmax               = 0
      self._Xmin               = 0
      self._Ymax               = 0
      self._Ymin               = 0
      self._Zmax               = 0
      self._Zmin               = 0
      if self._version >= (1, 3):
        self._StartOfWaveformDataPacketRec = 0
      if self._version >= (1, 4):
        self._StartOfFirstExtendedVLR     = 0
        self._NumberOfExtendedVLR         = 0
        self._NumberOfPointRecords        = 0
        self._NumberOfPointsByReturn      = (
          (0, ) * self._getN4Key('NumberOfPointsByReturn'))

      # No Variable Length Records yet
      self._varLenRecords = []

    # Create Point Data Record instance
#    self._PointDataRecord = PointDataRecord(self)

  def __repr__(self):
    """ Return :class:`Header` string representation
    """
    s = '%s: Ver. %d.%d - %d bytes\n' % (
      (self.__class__.__name__, ) + self._version + (self.sizeBytes(),))
    ml = getLongest([item[0] for item in self.structure])
    ff = " %%-%ds : %%s\n" % ml
    ffc = " %%-%ds : '%%s'\n" % ml
    for key, dtype, n in self.structure:
      if 'GUID4' in key:
        s += ff % ('Project ID / GUID', self._guid)
      if '_GUID' in key:
        continue
      if dtype in (self.CHAR, self.UCHAR):
        if n == 1:
          # Format is char but content is a NUMBER!
          s += ff % (key, self.__getattribute__('_%s' % key))
        else:
          sclean = str(string_clean(self.__getattribute__('_%s' % key)))
          s += ffc % (key, sclean.ljust(self._getN4Key(key)))
      else:
        s += ff % (key, string_clean(self.__getattribute__('_%s' % key)))
    if self._warnings:
      s += "WARNINGS.....\n" + self._warnings
    return s

  def _read(self):
    """ Read LAS header from file
    """
    if not self.isReadMode():
      raise IOError("%s not opened for reading" %
        self.__class__.__name__)

    for key, dtype, n in self.structure:
      nbytes = struct.calcsize(dtype) * n
      if dtype in (self.CHAR, self.UCHAR):
        # string
        value = self.fid().read(n)
        if n == 1:
          # Format is char but content is a NUMBER!
          value = struct.unpack(dtype, value)[0]
      else:
        if n > 1:
          # tuple
          value = struct.unpack(dtype * n, self.fid().read(nbytes))
        else:
          # single binary number
          value = struct.unpack(dtype, self.fid().read(nbytes))[0]

          # Handle LAS 1.2 GlobalEncoding bit fields
          if (self._version >= (1, 2)) and (key == 'GlobalEncoding'):
            value = (1 << 15) & value

      # Set attribute according to Header format
      self.__setattr__('_%s' % key, value)
    if not self._PointDataFormatID in self.validPointDataFormats[self._version]:
      self._warnings += (
        "--> PointDataFormatID %d is invalid for version %s.%s\n" %
        ((self._PointDataFormatID,) + self._version))

  def _setPDFID(self, pointDataFormatID):
    """ Set Point Data Format ID

      :param pointDataFormatID: Point Data Format ID
      :type  pointDataFormatID: int
      :raises: :class:`~ALASpy.lasExceptions.InvalidPointDataFormatID`
    """
    self._PointDataFormatID  = pointDataFormatID
    if pointDataFormatID == 0:
      self._PointDataReclen  = 20
    elif pointDataFormatID == 1:
      self._PointDataReclen  = 28
    elif pointDataFormatID in (2, 3) and self._version >= (1, 2):
      if pointDataFormatID == 2:
        self._PointDataReclen  = 26
      else:
        self._PointDataReclen  = 34
    else:
      raise InvalidPointDataFormatID(
        "PointDataFormatID '%d' is invalid for version %d.%d" %
        ((pointDataFormatID, ) + self._version))

  def _getN4Key(self, key):
    """ Return number of elements for `key' in header structure

      :returns: number of elements for `key' in header structure or None
      :rtype: int
    """
    for k, dtype, n in self.structure:
      if key == k:
        return n
    return None

  def _setWkt(self):
    """ Set WKT string to header

      :returns: False if self._wkt is False
      :rtype: bool
    """
    if self._wkt:
      geoKeys, geoDoubleParams, geoAsciiParams = geoKeysFromWkt(self._wkt)

      # Create Mandatory Variable Length Records with Georeferencing Information
      self._varLenRecords = []
      if geoKeys:
        vlr = VLRecord(self, userID=VLRecord.LASProjection,
          recordID=VLRecord.GeoKeyDirectoryKey, data=geoKeys)
        self.addVarLengthRecord(vlr)
      if geoDoubleParams:
        vlr = VLRecord(self, userID=VLRecord.LASProjection,
          recordID=VLRecord.GeoDoubleParamsKey, data=geoDoubleParams)
        self.addVarLengthRecord(vlr)
      if geoAsciiParams:
        vlr = VLRecord(self, userID=VLRecord.LASProjection,
          recordID=VLRecord.GeoAsciiParamsKey, data=geoAsciiParams)
        self.addVarLengthRecord(vlr)
      return True
    else:
      return False

#-------------------------------------------------------------------------------
# Public methods
#-------------------------------------------------------------------------------
  def parent(self):
    """ Return parent i.e. :class:`~ALASpy.las.LasObject` instance

      :returns: parent object
      :rtype: :class:`~ALASpy.las.LasObject`
    """
    return self._parent

  def isReadMode(self):
    """ Return True if Header is in 'read' mode

      :returns: True if Header is in 'read' mode
      :rtype: bool
    """
    return self.fid().mode.startswith('r')

  def write(self):
    """ Write LAS Header AND Variable Length Records (if any) to file
    """
    if self.isReadMode():
      raise IOError("%s not opened for writing" %
        self.__class__.__name__)

    for key, dtype, size in self.structure:
      nbytes = struct.calcsize(dtype) * size
      try:
        value = self.__getattribute__('_%s' % key)
        if dtype in (self.CHAR, self.UCHAR):
          if size == 1:
            # Format is char but content is a NUMBER!
            s = chr(value)
          else:
            # string, keep it
            s = value
        else:
          if size > 1:
            # tuple
            s = struct.pack(dtype * len(value), *value)
          else:
            # single binary number
            s = struct.pack(dtype, value)
      except:
        raise ValueError("Excepion on field <%s>" % key)
      self.fid().write(s)

    # Write Variable Length Records (if any)
    for vlr in self._varLenRecords:
      vlr.write()

  def close(self):
    """ Close LAS file object
    """
    if self.fid():
      self.fid().close()

  def fid(self):
    """ Return LAS file object

      :returns: LAS file object
      :rtype: :class:`file`
    """
    if hasattr(self._parent, 'fid'):
      return self._parent.fid()
    else:
      return self._parent

  def offsets(self):
    """ Return (x, y, z) offsets tuple

      :returns: (Xoffset, Yoffset, Zoffset)
      :rtype: tuple
    """
    return self._Xoffset, self._Yoffset, self._Zoffset

  def setOffsets(self, ox, oy, oz):
    """ Set coordinate offsets

      :param ox: x offset
      :type  ox: float
      :param oy: y offset
      :type  oy: float
      :param oz: z offset
      :type  oz: float
    """
    self._Xoffset = ox
    self._Yoffset = oy
    self._Zoffset = oz

  def scaleFactors(self):
    """ Return (x, y, z) scale factors tuple

      :returns: (XscaleFactor, YscaleFactor, ZscaleFactor)
      :rtype: tuple
    """
    return self._XscaleFactor, self._YscaleFactor, self._ZscaleFactor

  def version(self):
    """ Return LAS File version tuple

      :returns: Las file version tuple
      :rtype: tuple
    """
    return self._version

  def xRange(self):
    """ Return x coordinate range

      :returns: (Xmin, Xmax)
      :rtype: tuple
    """
    return self._Xmin, self._Xmax

  def yRange(self):
    """ Return y coordinate range

      :returns: (Ymin, Ymax)
      :rtype: tuple
    """
    return self._Ymin, self._Ymax


  def zRange(self):
    """ Return z coordinate range

      :returns: (Zmin, Zmax)
      :rtype: tuple
    """
    return self._Zmin, self._Zmax

  def setXRange(self, xmin, xmax):
    """ Set x coordinate range

      :param xmin: x coordinate minimum value
      :type  xmin: float
      :param xmax: x coordinate maximum value
      :type  xmax: float
    """
    self._Xmin = xmin
    self._Xmax = xmax

  def setYRange(self, ymin, ymax):
    """ Set y coordinate range

      :param ymin: y coordinate minimum value
      :type  ymin: float
      :param ymax: y coordinate maximum value
      :type  ymax: float
    """
    self._Ymin = ymin
    self._Ymax = ymax

  def setZRange(self, zmin, zmax):
    """ Set z coordinate range

      :param zmin: z coordinate minimum value
      :type  zmin: float
      :param zmax: z coordinate maximum value
      :type  zmax: float
    """
    self._Zmin = zmin
    self._Zmax = zmax

  def setRanges(self, minXYZ, maxXYZ):
    """ Set x, y, z coordinate ranges

      :param minXYZ: x, y, z coordinates minimum value
      :type  minXYZ: tuple
      :param maxXYZ: x, y, z coordinates maximum value
      :type  maxXYZ: tuple
    """
    self._Xmin, self._Ymin, self._Zmin = minXYZ
    self._Xmax, self._Ymax, self._Zmax = maxXYZ

  def setProjectGUID(self, guid):
    """ Set Project ID GUID fields

      :param guid: uuid
      :type  guid: :class:`uuid.UUID` version 4 instance
      :raises: :class:`IOError`, :class:`TypeError`
    """
    if self.isReadMode():
      raise IOError("Cannot set Project ID GUID: file is opened for reading")

    if isinstance(guid, uuid.UUID) and guid.version == 4:
      self._guid = guid
      self._ProjectID_GUID1 = struct.unpack(
        AbstractRecord.ULONG, self._guid.bytes[:4])[0]
      self._ProjectID_GUID2 = struct.unpack(
        AbstractRecord.USHORT, self._guid.bytes[4:6])[0]
      self._ProjectID_GUID3 = struct.unpack(
        AbstractRecord.USHORT, self._guid.bytes[6:8])[0]
      self._ProjectID_GUID4 = self._guid.bytes[8:]
    else:
      raise TypeError("Argument must be uuid.UUID version 4 instance")

  def addVarLengthRecord(self, vlr):
    """ Add Varable Length Record

      :param vlr: Varable Length Record
      :type  vlr: :class:`VLRecord`
    """
    self._varLenRecords.append(vlr)
    self._NumVarLenRecords = len(self._varLenRecords)
    self._OffsetToPointData += vlr.sizeBytes()

  def varLengthRecords(self):
    """ Return Varable Length Record list

      :returns: Varable Length Record list
      :rtype: list
    """
    return self._varLenRecords

  def getVarLenRecord(self, user_id, record_id):
    """ Return Varable Length Record with with User_ID `user_id`
        and Record_ID `record_id`

      :param user_id: User_ID
      :type  user_id: string
      :param record_id: Record_ID
      :type  record_id: int
      :returns: Varable Length Record
      :rtype: :class:`VLRecord`
    """
    for r in self._varLenRecords:
      if r.getID() == (user_id, record_id):
        return r

  def wkt(self):
    """ Return Well Known Text string or None

      :returns: WKT
      :rtype: string
    """
    if osr is None:
      return None

    if self._wkt:
      return self._wkt

    if self._varLenRecords:
      geoKeyDirectory = self.getVarLenRecord(
        VLRecord.LASProjection, VLRecord.GeoKeyDirectoryKey)
      geoDoubleParams = self.getVarLenRecord(
        VLRecord.LASProjection, VLRecord.GeoDoubleParamsKey)
      geoAsciiParams = self.getVarLenRecord(
        VLRecord.LASProjection, VLRecord.GeoAsciiParamsKey)
      if geoKeyDirectory:
        self._wkt = normalize(geoKeyDirectory, geoDoubleParams, geoAsciiParams)
    return self._wkt

  def prettyWkt(self):
    """ Return pretty WKT string or None

      :returns: WKT
      :rtype: string
    """
    wkt = self.wkt()
    if wkt:
      srs = osr.SpatialReference()
      srs.ImportFromWkt(wkt)
      return srs.ExportToPrettyWkt()
    else:
      return None

  def proj4(self):
    """ Return Proj4 string or None

      :returns: Proj4
      :rtype: string
    """
    if self._proj4:
      return self._proj4

    wkt = self.wkt()
    if wkt:
      srs = osr.SpatialReference()
      srs.ImportFromWkt(wkt)
      self._proj4 = srs.ExportToProj4()
    return self._proj4

  def setProj4(self, proj4):
    """ Set Proj4 string to header

      :param proj4: Proj.4 string
      :type  proj4: string
      :returns: False if :class:`osr` module is not available
      :rtype: bool

      .. note:: uses osr.SpatialReference() validation: ImportFromProj4 and
         ExportToProj4
      ..
    """
    if osr is None:
      return False

    srs = osr.SpatialReference()
    srs.ImportFromProj4(proj4)
    self._wkt = srs.ExportToWkt()
    return self._setWkt()

  def setWkt(self, wkt):
    """ Set WKT string to header

      :param wkt: WKT
      :type  wkt: string
      :returns: False if :class:`osr` module is not available
      :rtype: bool

      .. note:: uses osr.SpatialReference() validation: ImportFromWkt and
         ExportToWkt
      ..
    """
    if osr is None:
      return False

    srs = osr.SpatialReference()
    srs.ImportFromWkt(wkt)
    self._wkt = srs.ExportToWkt()
    return self._setWkt()

#  def pointDataRecordDtype(self, *args, **kargs):
#    """ Return Point Data Record Dtype
#
#      :returns: Point Data Record Dtype
#      :rtype: :class:`numpy.dtype`
#    """
#    return self._PointDataRecord.makeDtype(*args, **kargs)
#
#  def pointDataRecordSize(self):
#    """ Return Point Data Record size in bytes
#
#      :returns: Point Data Record size in bytes
#      :rtype: int
#    """
#    return self._PointDataRecord.sizeBytes()

#===============================================================================
class VLRecord(AbstractRecord):
  """ The Variable Length Record item
  """
  LASProjection = 'LASF_Projection'
  User_Defined  = 32767
  # Key number 1286 registered by
  # Roberto Vidmar rvidmar@inogs.it since 20121010 to:
  # OGS (Istituto Nazionale di Oceanografia e di Geofisica Sperimentale)
  OGS = 'OGS'

  # Mandatory GeoTff tags
  GeoKeyDirectoryKey = 34735
  GeoDoubleParamsKey = 34736
  GeoAsciiParamsKey  = 34737
  GeoKeyDirectoryTag = (LASProjection, GeoKeyDirectoryKey)
  GeoDoubleParamsTag = (LASProjection, GeoDoubleParamsKey)
  GeoAsciiParamsTag  = (LASProjection, GeoAsciiParamsKey)

  Structure = (
    ('Reserved',           AbstractRecord.USHORT,   1),
    ('User_ID',            AbstractRecord.CHAR,    16),
    ('Record_ID',          AbstractRecord.USHORT,   1),
    ('RecLenAfterHeader',  AbstractRecord.USHORT,   1),
    ('Description',        AbstractRecord.CHAR,    32))

  Descriptions = dict((
    (1024, dict((
      (1, 'ModelTypeProjected'),
      (2, 'ModelTypeGeographic'),
      (3, 'ModelTypeGeocentric'),
      ))),
    (1025, dict((
      (1, 'RasterPixelIsArea'),
      (2, 'RasterPixelIsPoint'),
      ))),
    ))

  GeoKey = (
    ('wKeyDirectoryVersion', AbstractRecord.USHORT, 1),
    ('wKeyRevision',         AbstractRecord.USHORT, 1),
    ('wMinorRevision',       AbstractRecord.USHORT, 1),
    ('wNumberOfKeys',        AbstractRecord.USHORT, 1))

  KeyEntry = (
    ('wKeyID',           AbstractRecord.USHORT, 1),
    ('wTIFFTagLocation', AbstractRecord.USHORT, 1),
    ('wCount',           AbstractRecord.USHORT, 1),
    ('wValue_Offset',    AbstractRecord.USHORT, 1))

  def __init__(self, header, userID='', recordID=0, description='', data=None):
    """ Create a Variable Length Record instance

      :param header: LAS Header
      :type  header: :class:`Header`
      :param userID: user ID
      :type  userID: string
      :param recordID: record ID
      :type  recordID: int
      :param description: record description
      :type  description: string
      :param data: payload of the record
      :type  data: tuple or string according to (userID, recordID)

      .. note:: `data` is used only if LAS file is opened for writing

      .. note:: if userID is :class:`VLRecord.OGS` `data` can be any Python
                object that can be serialized by `cPickle.dumps`
    """
    self._header = header
    self._createStructure(self.Structure)
    self._warnings = ''
    if header.isReadMode():
      fid = header.fid()
      super(VLRecord, self)._read(fid)
      # Now read payload data
      self._Data = fid.read(self._RecLenAfterHeader)
    else:
      self._Reserved = 0
      self._User_ID = userID
      self._Record_ID = recordID
      self._Description = description
      self._RecLenAfterHeader = 0
      recID = self.getID()
      if recID == self.GeoDoubleParamsTag:
        self._Data = struct.pack(AbstractRecord.DOUBLE * len(data), *data)
      elif recID == self.GeoKeyDirectoryTag:
        # data is a tuple of geokeys tuples:
        geoKeys = data
        # Set payload
        self._Data = ''
        j = 0
        for key, dtype, size in self.GeoKey:
          self._Data += struct.pack(dtype * size, geoKeys[0][j])
          j += 1
        # Convert key entries
        for gk in geoKeys[1:]:
          j = 0
          for key, dtype, size in self.KeyEntry:
            self._Data += struct.pack(dtype * size, gk[j])
            j += 1
      elif recID[0] == self.OGS:
        # data is a Python Object
        self._Data = cPickle.dumps(data)
      else:
        self._Data = data

    if self.getID()[0] != self.OGS and not self.getID() in (
      self.GeoKeyDirectoryTag, self.GeoDoubleParamsTag, self.GeoAsciiParamsTag):
      # All other vlr records
      self._warnings += (
        "  --> Unknown Variable Length Record with "
        "User ID '%s' and Record ID '%s'\n" % self.getID())

    if self._RecLenAfterHeader == 0:
      self._RecLenAfterHeader = self.tagSizeBytes()

  def __repr__(self):
    """ Unambiguous representation of a :class:`VLRecord`

      :raises: :class:`ValueError`
    """
    headerSize = super(self.__class__, self).sizeBytes()
    s = '%s: Record_ID %d: %d bytes (Header %d + Data %d)\n' % (
      self.__class__.__name__, self._Record_ID, self.sizeBytes(),
      headerSize, self._RecLenAfterHeader)
    ml = getLongest([item[0] for item in self.Structure])
    ff = '  %%-%ds : %%s\n' % ml
    for key, dtype, n in self.Structure:
      s += ff % ('%s' % key, string_clean(self.__getattribute__('_%s' % key)))
    # Tag descripion and length
    s += ff % (self.tagName(), "%d bytes" % self.tagSizeBytes())
    recID = self.getID()
    if recID[0] == self.OGS:
      # Special OGS VLRecord, payload is a Python object!
      s += "  %s\n" % self.getData().__repr__()
    if recID == self.GeoDoubleParamsTag:
      s += "  \'%s\'\n" % ", ".join(["%.4f" % p for p in self.getData()])
    elif recID == self.GeoAsciiParamsTag:
      s += "  %s\n" % convert(self.getData())
    elif recID == self.GeoKeyDirectoryTag:
      geoKeys = self.getData()
      s += ("  Version: %d\n  Key_Revision: %d.%d\n  Number of keys: %d\n"
        "  Keyed information:\n" % geoKeys[0])
      i = 1
      for wKeyID, wTIFFTagLocation, wCount, wValue_Offset in geoKeys[1:]:
        s += "  %2d) %s" % (i, GeotiffKeys[wKeyID])
        i += 1
        if wTIFFTagLocation == 0:
          # 0 means data in the wValue_Offset field is an unsigned short
          s += " (Short, %d):" % wCount
          if wKeyID in self.Descriptions:
            s += " %s" % self.Descriptions[wKeyID][wValue_Offset]
          else:
            s += " %s" % GeotiffKeys[wValue_Offset]
        elif wTIFFTagLocation == self.GeoDoubleParamsKey:
          # Data is located at index wValue_Offset of the
          # GeoDoubleParamsTag
          s += " (Double, %d):" % wCount
          vlr = self.header().getVarLenRecord(
            VLRecord.LASProjection, wTIFFTagLocation)
          s += ' %s' % vlr.getData()[wValue_Offset]
        elif wTIFFTagLocation == VLRecord.GeoAsciiParamsKey:
          # Data is located at index wValue_Offset of the
          # GeoAsciiParamsTag
          s += " (Ascii, %d):" % wCount
          vlr = self.header().getVarLenRecord(
            VLRecord.LASProjection, wTIFFTagLocation)
          # We take 1 character less because strings should be null terminated
          s += ' "%s"' % vlr.getData()[
            wValue_Offset: wValue_Offset + wCount - 1]
        else:
          raise ValueError(
            "Invalid wTIFFTagLocation %d" % wTIFFTagLocation)
        s += "\n"
    else:
      pass
    if self._warnings:
      s += "  WARNINGS.....\n" + self._warnings
    return s

#-------------------------------------------------------------------------------
# Public methods
#-------------------------------------------------------------------------------
  def getData(self):
    """ Return payload data

    :returns: payload data
    :rtype: * string or
            * tuple
            * Python Object (if userID is :class:`VLRecord.OGS`)

            according to (userID, recordID)

    .. note:: If this record is a :class:`VLRecord.GeoKeyDirectoryTag`
        returns something like this::

          (
          # a Version 1 GeoTIFF GeoKey, Rev. 1.0, 7 Keys
          (1,        1,      0,     7),
          #1024 = GTModelTypeGeoKey, 0=short, count = 1 Projected = 1, Offset 1
          (1024,     0,      1,     1),
          #1025 = GTRasterTypeGeoKey, 0=short, count=1, Projected=1, Offset 1
          (1025,     0,      1,     1),
          #1026 = GTCitationGeoKey, recordId 34737, 33 Bytes, Offset 0
          (1026, 34737,     33,     0),
          #2049 = GeogCitationGeoKey, recordId 34737, 7 bytes, Offset 33
          (2049, 34737,      7,    33),
          #2054 = GeogAngularUnitsGeoKey, 0=short, count=1, Ofset 9102
          (2054,     0,      1,  9102),
          (3072,     0,      1, 32630),
          (3076,     0,      1,  9001),
          )

    """
    recID = self.getID()
    if recID == self.GeoDoubleParamsTag:
      data = struct.unpack(
      (len(self._Data) / struct.calcsize(AbstractRecord.DOUBLE)) *
      AbstractRecord.DOUBLE, self._Data)
    elif recID == self.GeoKeyDirectoryTag:
      payload = self._Data
      gkKeys = list()
      i = 0
      geoKey = ()
      # Read directory
      for key, dtype, size in self.GeoKey:
        nbytes = struct.calcsize(dtype) * size
        geoKey += (struct.unpack(dtype, payload[i: i + nbytes])[0], )
        i += nbytes
      gkKeys = (geoKey, )
      # Read key entries
      for dummy in xrange(geoKey[-1]):
        keyEntry = ()
        for key, dtype, size in self.KeyEntry:
          nbytes = struct.calcsize(dtype) * size
          keyEntry += (struct.unpack(dtype, payload[i: i + nbytes])[0], )
          i += nbytes
        gkKeys += (keyEntry, )
      data = gkKeys
    elif recID[0] == self.OGS:
      data = cPickle.loads(self._Data)
    else:
      data = self._Data

    return data

  def header(self):
    """ Return Header instance

      :returns: header
      :rtype: :class:`Header`
    """
    return self._header

  def write(self):
    """ Write instance to file
    """
    #self._Description = 'GTag'
    fid = self._header.fid()

    for key, dtype, n in self.Structure:
      if dtype in (self.CHAR, self.UCHAR) and n > 1:
        value = pad_with_nulls(self.__getattribute__('_%s' % key), n)
        s = ''
        for byte in value:
          s += struct.pack(dtype, byte)
      else:
        value = self.__getattribute__('_%s' % key)
        s = struct.pack(dtype, value)
      fid.write(s)
    fid.write(self._Data)

  def getID(self):
    """ Return unique Record ID tuple

      :returns: User_ID, Record_ID
      :rtype: tuple
    """
    return nonull(self._User_ID), self._Record_ID

  def sizeBytes(self):
    """ Return size in bytes of this VLR

      :returns: size in bytes of this VLR
      :rtype: int
    """
    return self._RecLenAfterHeader + super(VLRecord, self).sizeBytes()

  def tagName(self):
    """ Return Tag name of this VLR

      :returns: Tag name of this VLR
      :rtype: string
    """
    recID = self.getID()
    if recID == self.GeoKeyDirectoryTag:
      name = "GeoKeyDirectoryTag"
    elif recID == self.GeoDoubleParamsTag:
      name = "GeoDoubleParamsTag"
    elif recID == self.GeoAsciiParamsTag:
      name = "GeoAsciiParamsTag"
    else:
      name = "User Defined Tag"
    return name

  def tagSizeBytes(self):
    """ Return Tag size in bytes for this VLR

      :returns: size in bytes of payload data
      :rtype: int
    """
    return len(self._Data)

  def isReadMode(self):
    """ Return True if Header is in 'read' mode

      :returns: True if Header is in 'read' mode
      :rtype: bool
    """
    return self._header.isReadMode()

#===============================================================================
def main():
  pn = 'nicola_esempio.las'
  pn = '/d0/Downloads/libLAS-1.7.0/test/data/1.0_0.las'
  print Header(open(pn, 'r'))
  newpn = 'lasheader.las'
  new = Header(open(newpn, 'w'), version=(1, 4))
  print "THE NEW HEADER\n", new
  new.write()
  new.close()
  new1 = Header(open(newpn, 'r'))
  print "HOW THE NEW HEADER LOOKS\n", new1
  print "Done"

#===============================================================================
if __name__ == '__main__':
  main()
